TCA が 1.0 になって対応したこと
いきなり 1.0 にアップデートせずに、まずは 0.58.0 で移行作業を行うのがおすすめ。
(参考: TCA migration questions and some experiences from 0.54 to 1.0 · pointfreeco/swift-composable-architecture · Discussion #2345 · GitHub)
0.58.0 であれば Deprecations は Warning として教えてくれるので、移行しやすい。
いきなり 1.0 にアップデートしてしまうとコンパイルができないためにすべての場所を教えてくれるわけじゃないし、関係ない別のエラーが出たりする。。
ReducerProtocol
-> Reducer
置換で一括対応でビルドすれば特に漏れなく対応できた。
// Before
struct Fetaure: ReducerProtocol { /* ... */ }
var body: some ReducerProtocolOf<Self> { /* ... */ }
// After
struct Fetaure: Reducer { /* ... */ }
var body: some ReducerOf<Self> { /* ... */ }
EffectTask
-> Effect
一部 EffectTask
が残ってたので Effect
に置き換え。
(.fireAndForget
とか久々に目にした)
// Before
return .fireAndForget {
doSomething()
}
return .task {
let something = await getSomething()
return .somethingResponse(something)
}
// After
return .run { _ in
doSomething()
}
return .run { send in
let something = await getSomething()
await send(.somethingResponse(something))
}
store.stateless
をやめる
Action を send
するためだけで状態が変わらないボタン等で Store.stateless
を利用していたが、 WWDC 2023 で発表された Observation に備えて Store.send(_:)
が追加され、これに合わせて Store.stateless
は削除された。
より詳しい Discussion としては RFC: Add `Store.send` and `Store.withState` · pointfreeco/swift-composable-architecture · Discussion #2223 · GitHub を参考。
Store.send(_:)
のドキュメントを見ると、 ViewStore
が存在する場合はそちらの send
が望ましいとのこと [1] なので、すべての send
を置き換える必要はない。
If a view store is available, prefer
send(_:)
.
// Before
WithViewStore(store.stateless) { viewStore in
Button {
viewStore.send(.someAction)
} label: {
Text("Some button")
}
}
// After
Button {
store.send(.someAction)
} label: {
Text("Some button")
}
Store の Reducer は builder block 使う
Add `Store.init` that takes reducer builder by stephencelis · Pull Request #2087 · pointfreeco/swift-composable-architecture · GitHub で導入されていて、 builder を使わない init
は soft-deprecation になっていたが、削除された。
個人的には ReducerBuilder
の中身みたいな書き味になって結構気に入っている。
var body: some ReducerOf<Feature> {
Scope(state: \.child, action: /Action.child) {
Child()
}
Reduce { /* ... */ }
}
変更としては以下の形。 Trailing closure の利用有無で 2 パターンに別れた。
// Bofore
ExampleView(
store: .init(
initialState: .init(),
reducer: Example()
)
)
// After (変更が最小)
ExampleView(
store: .init(
initialState: .init(),
reducer: { Example() }
)
)
// After (trailing closure を使う)
ExampleView(
store: .init(initialState: .init()) {
Example()
}
)
WithViewStore(_:)
-> WithViewStore(_:observe:)
TCA をよくわかってないときの実装が残っていた。 (今もよくわかってない)
WithViewStore(store)
は WithViewStore(store, observe: { $0 })
と同じなので、末端の View や小さい State 以外では避けて、適切なスコープだけを監視するようにして、パフォーマンスに気を使うようにしたい。
// Before
WithViewStore(store) { viewStore in /* ... */ }
// After
WithViewStore(store, observe: \.child) { viewStore in /* ... */ }
// After (以下のように State すべて監視するときは末端に近い View もしくは State が小さい時だけにする。)
WithViewStore(store, observe: { $0 }) { viewStore in /* ... */ }
alert(_:dismiss:)
の廃止
alert(_:dismiss:) - ComposableArchitecture Documentation が削除された。
PresentationState
を使った実装に変更した。
Effect.send
を適切に使う
(1.0 はあまり関係ないがこの機に一緒にやったのでメモ)
.send
の存在を知らなかったのか、後から Delegate action を導入したのかは覚えてないが、 Effect.run
の中でただ send
だけしているものがあったので Delegate action については [2] Effect.send
を使うようにした。
We do not recommend using
Effect.send
to share logic. Instead, limit usage to child-parent communication, where a child may want to emit a “delegate” action for a parent to listen to.
inout state
は Concurrency でキャプチャできないので、キャプチャリストを使って無理やり Delegate action を実行していた実装もあったので、合わせて修正した。
// Before
return .run { send in await send(.delegate(.something)) }
return .run { [childState = state.child] send in
await send(.delegate(.delegationWithParameter(childState)))
}
// After
return .send(.delegate(.something))
return .send(.delegate(.delegationWithParameter(state.child)))
疑問が残るところ
Preview の中で Store を初期化する際に、 initialState
のパラメータとして空配列を渡したいときに何故か []
だとコンパイラーになってしまった。
エラーの内容としては "Cannot convert value of type [Any]
to expected argument type [Item]
" のような感じ。
型推論が効いてない感じなので、とりあえず型を明示して [Item]()
のように初期化を行った。
// Before
struct ExampleView_Previews: PreviewProvider {
static var previews: some View {
Example(
store: .init(
initialState: .init(items: []),
reducer: Example()
)
)
}
}
// After
struct ExampleView_Previews: PreviewProvider {
static var previews: some View {
Example(
store: .init(
initialState: .init(items: [Item]()), // ここ
reducer: { Example() }
)
)
}
}